一、神经网络训练过程
神经网络的训练是一个迭代优化的过程,目标是找到一组最优的权重参数,使得网络的预测输出尽可能接近真实目标值。整个过程可以分为三个核心步骤:
-
前向传播 (Forward Propagation)
输入数据通过网络各层依次计算,每一层执行线性变换和激活函数,最终得到预测输出。Y = f(X · W + b) -
计算损失 (Loss Calculation)
使用损失函数衡量预测值与真实值之间的差距。本例使用均方误差 (MSE):Loss = 1 n × Σ(Ypred − Ytarget)² -
反向传播 (Backpropagation)
根据链式法则,计算损失对每个参数的梯度,然后使用梯度下降更新权重:W = W − lr × ∂Loss ∂W
梯度计算详解
以单层线性网络为例,推导 ∂Loss/∂W 的计算过程:
已知:
- 前向传播:Y = X · W + b
- 损失函数:Loss = 1n Σ(Y − Ytarget)²
Step 1:计算 Loss 对 Y 的梯度
Step 2:应用链式法则,计算 Loss 对 W 的梯度
由于 Y = X · W + b,所以 ∂Y/∂W = XT
Step 3:计算 Loss 对 b 的梯度
由于 Y = X·W + b,对 b 求偏导时,X·W 是常数:
直观理解:b 增加 1,Y 就增加 1,变化率是 1:1,所以导数 = 1
因此,梯度为各样本梯度之和:
Step 4:更新参数
多层网络:对于多层神经网络,梯度通过链式法则从输出层逐层向输入层传递,每层依次计算。这就是"反向传播"名称的由来。
二、完整源代码
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
# ============== 可修改参数 ==============
# 网络结构:每个数字代表该层的神经元数量
HIDDEN_LAYERS = [64, 32]
# 激活函数选择: 'relu', 'tanh', 'sigmoid', 'leaky_relu', 'elu'
ACTIVATION = 'relu'
# 训练参数
lr = 0.01 # 学习率
epochs = 2000 # 最大训练轮数
num_samples = 200 # 训练样本数量
early_stop_patience = 50 # 早停耐心值
early_stop_threshold = 0.01 # 早停阈值
# ========================================
# 激活函数映射
ACTIVATION_MAP = {
'relu': nn.ReLU(),
'tanh': nn.Tanh(),
'sigmoid': nn.Sigmoid(),
'leaky_relu': nn.LeakyReLU(0.1),
'elu': nn.ELU(),
}
def build_network(input_dim, hidden_layers, output_dim, activation_name):
"""动态构建多层神经网络"""
layers = []
prev_dim = input_dim
activation = ACTIVATION_MAP.get(activation_name, nn.ReLU())
# 添加隐藏层
for hidden_dim in hidden_layers:
layers.append(nn.Linear(prev_dim, hidden_dim))
layers.append(activation)
prev_dim = hidden_dim
# 输出层(不加激活函数,因为是回归任务)
layers.append(nn.Linear(prev_dim, output_dim))
return nn.Sequential(*layers)
# 生成训练数据: y = x²
X = torch.linspace(-5, 5, num_samples).reshape(-1, 1)
Y_target = X ** 2
# 构建网络
model = build_network(1, HIDDEN_LAYERS, 1, ACTIVATION)
# 优化器和损失函数
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
loss_fn = nn.MSELoss()
# 训练循环
loss_history = []
best_loss = float('inf')
patience_counter = 0
for epoch in range(epochs):
# 1. 前向传播
Y_pred = model(X)
loss = loss_fn(Y_pred, Y_target)
# 2. 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 记录和早停检查
current_loss = loss.item()
loss_history.append(current_loss)
if current_loss < best_loss:
best_loss = current_loss
patience_counter = 0
else:
patience_counter += 1
if patience_counter >= early_stop_patience and current_loss < early_stop_threshold:
print(f"早停触发!")
break
print(f"训练完成! 最终 Loss: {loss_history[-1]:.6f}")
三、训练输出结果
================================================== 网络结构: Sequential( (0): Linear(in_features=1, out_features=64, bias=True) (1): ReLU() (2): Linear(in_features=64, out_features=32, bias=True) (3): ReLU() (4): Linear(in_features=32, out_features=1, bias=True) ) ================================================== 隐藏层: [64, 32] 激活函数: relu 学习率: 0.01, 训练轮数: 2000 ================================================== 开始训练... Epoch 100 | Loss: 1.653280 | Best: 1.653280 Epoch 200 | Loss: 0.249146 | Best: 0.249146 Epoch 300 | Loss: 0.056769 | Best: 0.056769 Epoch 400 | Loss: 0.018616 | Best: 0.018616 Epoch 500 | Loss: 0.008368 | Best: 0.008368 Epoch 600 | Loss: 0.004815 | Best: 0.004815 Epoch 700 | Loss: 0.003291 | Best: 0.003291 Epoch 800 | Loss: 0.002482 | Best: 0.002482 Epoch 900 | Loss: 0.001994 | Best: 0.001994 Epoch 1000 | Loss: 0.001664 | Best: 0.001664 Epoch 1100 | Loss: 0.001435 | Best: 0.001435 Epoch 1200 | Loss: 0.001262 | Best: 0.001262 Epoch 1300 | Loss: 0.001136 | Best: 0.001136 Epoch 1400 | Loss: 0.001030 | Best: 0.001030 Epoch 1500 | Loss: 0.000943 | Best: 0.000943 Epoch 1600 | Loss: 0.000875 | Best: 0.000875 Epoch 1700 | Loss: 0.000819 | Best: 0.000819 Epoch 1800 | Loss: 0.000770 | Best: 0.000770 Epoch 1900 | Loss: 0.000726 | Best: 0.000726 Epoch 2000 | Loss: 0.000685 | Best: 0.000685 ================================================== 训练完成! 最佳 Loss: 0.000685 (Epoch 2000) 最终 Loss: 0.000685 ==================================================
图:左侧为拟合效果对比,右侧为 Loss 下降曲线
四、结果分析
4.1 拟合效果
4.2 Loss 曲线分析
从 Loss 曲线可以观察到:
- 快速下降阶段 (0-300 epoch):Loss 从初始值快速下降,网络快速学习数据的主要模式
- 缓慢收敛阶段 (300-2000 epoch):Loss 下降速度变慢,网络在微调参数
- 最终收敛:Loss 稳定在 0.0007 左右,继续训练收益递减
五、代码实现详解
5.1 网络架构
| 层级 | 类型 | 输入维度 | 输出维度 | 说明 |
|---|---|---|---|---|
| 第1层 | Linear + ReLU | 1 | 64 | 扩展特征维度 |
| 第2层 | Linear + ReLU | 64 | 32 | 特征压缩 |
| 输出层 | Linear | 32 | 1 | 回归输出(无激活) |
为什么输出层不加激活函数?
这是回归任务的关键设计。激活函数会限制输出范围,而回归任务需要输出任意值:
| 激活函数 | 输出范围 | 问题 |
|---|---|---|
| ReLU | [0, +∞) | 无法输出负数 |
| Sigmoid | (0, 1) | 只能输出 0~1 |
| Tanh | (-1, 1) | 只能输出 -1~1 |
| 无激活 | (-∞, +∞) | 任意值 ✓ |
以本例 y = x² 为例:
- x 范围:[-5, 5]
- y 范围:[0, 25]
如果输出层加了 Tanh,最大只能输出 1,永远无法拟合到 25!
不同任务的输出层设计
# 回归任务(预测连续值):无激活
nn.Linear(32, 1) # 直接输出
# 二分类任务:Sigmoid(输出概率 0~1)
nn.Linear(32, 1)
nn.Sigmoid()
# 多分类任务:Softmax(输出概率分布)
nn.Linear(32, 10) # 10个类别
nn.Softmax(dim=1)
简单理解:
- 隐藏层:加激活函数 → 学习非线性特征
- 输出层:根据任务决定
- 回归 → 不加(要输出真实数值)
- 分类 → 加 Sigmoid/Softmax(要输出概率)
隐藏层为什么必须加激活函数?
不是"必须",但不加就失去了意义。
如果不加激活函数会怎样?
假设两层线性变换,不加激活函数:
这等价于:
多层线性变换 = 单层线性变换,网络再深也没用!
数学证明:
# 两层无激活
layer1: Y₁ = X · W₁ + b₁
layer2: Y₂ = Y₁ · W₂ + b₂
# 展开
Y₂ = (X · W₁ + b₁) · W₂ + b₂
= X · W₁ · W₂ + b₁ · W₂ + b₂
= X · W' + b' # 仍是线性!
激活函数的作用:
# 有激活函数
layer1: Y₁ = ReLU(X · W₁ + b₁) # 非线性变换
layer2: Y₂ = Y₁ · W₂ + b₂
# 无法合并!网络获得了表达非线性的能力
对比实验:
| 配置 | 能否拟合 y=x² |
|---|---|
| 1层无激活 | ❌ 只能拟合直线 |
| 10层无激活 | ❌ 仍只能拟合直线 |
| 2层有ReLU | ✓ 可以拟合曲线 |
5.2 关键组件
- 优化器 (Adam):自适应学习率优化器,结合了 Momentum 和 RMSprop 的优点
- 损失函数 (MSE):均方误差,适用于回归任务
- 早停机制:防止过拟合,当 Loss 不再下降时提前终止训练
常用优化器详解
AI训练中常用的优化器可分为三大类:
1. 基础优化器
- SGD(随机梯度下降):最基础的优化器,可加入 momentum 加速收敛
- SGD + Momentum:引入动量,帮助跳出局部最优
2. 自适应学习率优化器
- AdaGrad:对稀疏特征友好,但学习率会单调递减
- RMSprop:解决 AdaGrad 学习率过快衰减的问题
- Adam:结合 Momentum 和 RMSprop,目前最常用
- AdamW:Adam 的改进版,修正了权重衰减的实现方式,大模型训练中广泛使用
- Adafactor:节省内存的变体,适合超大模型
3. 新型优化器
- LAMB:适合大 batch 训练
- Lion:Google 提出,内存占用更低
- Sophia:二阶优化器,训练效率更高
PyTorch 中使用优化器
方式1:别名导入(推荐,更简洁)
import torch.optim as optim
# SGD + Momentum
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# Adam(通用首选)
optimizer = optim.Adam(model.parameters(), lr=1e-4)
# AdamW(大模型训练主流选择)
optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)
方式2:直接使用完整路径
import torch
# 效果与方式1完全相同
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)
两种写法效果完全一样,只是风格不同。大部分优化器只需设置学习率、权重衰减等参数即可。一些新型优化器如 Lion 可能需要额外安装库(如 pip install lion-pytorch)。
优化器对比表
| 优化器 | 全称 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|---|
| SGD | Stochastic Gradient Descent | 实现简单;泛化性能好;内存占用小 | 收敛慢;容易陷入局部最优;对学习率敏感 | 小规模模型;对泛化要求高的任务 |
| SGD + Momentum | SGD with Momentum | 加速收敛;减少震荡;有助于跳出局部最优 | 引入额外超参数;仍需仔细调参 | CNN图像分类;需要稳定泛化的任务 |
| AdaGrad | Adaptive Gradient Algorithm | 自动调整学习率;对稀疏特征友好 | 学习率单调递减,后期几乎停止学习 | NLP中的词嵌入;稀疏数据场景 |
| RMSprop | Root Mean Square Propagation | 解决学习率过快衰减;适合非平稳目标 | 仍需手动设置初始学习率 | RNN/LSTM训练;在线学习 |
| Adam | Adaptive Moment Estimation | 收敛快;对超参数不敏感;自适应学习率 | 泛化性能可能不如SGD;内存占用较大 | 通用首选;快速原型验证;Transformer |
| AdamW | Adam with Decoupled Weight Decay | 正确实现权重衰减;泛化更好;训练稳定 | 内存占用同Adam | 大语言模型训练(GPT、BERT等);当前主流 |
| Adafactor | Adaptive Factor | 大幅节省内存;适合超大模型 | 可能不如Adam稳定 | 显存受限时训练超大模型(如T5) |
| LAMB | Layer-wise Adaptive Moments for Batch training | 支持超大batch训练;收敛快 | 小batch效果不明显 | 分布式大规模训练;BERT预训练 |
| Lion | Evolved Sign Momentum | 内存占用比Adam少一半;更新规则简单 | 较新,实践经验较少;对学习率更敏感 | 视觉模型(ViT);追求内存效率 |
| Sophia | Second-order Clipped Stochastic Optimization | 二阶信息加速收敛;训练步数更少 | 计算开销略高;实现复杂 | LLM预训练;追求训练效率 |
优化器选择建议
| 场景 | 推荐优化器 |
|---|---|
| 快速实验 / 原型开发 | Adam |
| 大语言模型预训练 | AdamW |
| 显存紧张的大模型 | Adafactor 或 Lion |
| 追求最佳泛化(CV任务) | SGD + Momentum |
| 超大 batch 分布式训练 | LAMB |
六、训练参数设置指南
学习率 (lr = 0.01)
控制每次参数更新的步长大小。
推荐范围:0.001 ~ 0.1
训练轮数 (epochs = 2000)
完整遍历训练数据的次数。
推荐范围:500 ~ 5000
样本数量 (num_samples = 200)
训练数据点的数量。
推荐范围:100 ~ 1000
6.1 学习率 (lr) 设置不当的问题
lr 过大 (如 lr = 1.0)
- 现象:Loss 剧烈震荡,无法收敛,甚至变成 NaN
- 原因:参数更新步长太大,直接"跨过"最优解,在最优点附近来回震荡
lr 过小 (如 lr = 0.00001)
- 现象:Loss 下降极慢,需要大量 epoch 才能收敛
- 原因:每次更新的步长太小,像"蜗牛爬行"一样缓慢接近最优解
6.2 训练轮数 (epochs) 设置不当的问题
epochs 过少 (如 epochs = 50)
- 现象:欠拟合,Loss 还很高就停止了,预测曲线与目标偏差大
- 原因:网络还没学够,参数还未收敛到最优值
epochs 过多 (如 epochs = 100000)
- 现象:过拟合,在训练数据上表现好,但泛化能力下降;训练时间过长
- 原因:网络开始"记忆"训练数据的噪声,而非学习真实规律
6.3 样本数量 (num_samples) 设置不当的问题
样本过少 (如 num_samples = 10)
- 现象:在采样点上拟合好,但在未见过的点上预测差
- 原因:数据太稀疏,网络无法学习到完整的函数形态
样本过多 (如 num_samples = 100000)
- 现象:训练速度慢,内存占用大
- 原因:对于简单函数,过多样本是冗余的,增加计算开销
6.4 参数调优建议
最佳实践
- 从默认值开始:lr=0.01, epochs=1000, num_samples=200
- 观察 Loss 曲线:
- 如果震荡 → 降低 lr
- 如果下降太慢 → 提高 lr 或增加 epochs
- 如果很快就不再下降 → 可能需要更复杂的网络
- 使用早停机制:自动找到最佳停止点,避免过拟合
- 分阶段调参:先调 lr,再调 epochs,最后调网络结构
6.5 不同参数组合效果对比
| 参数组合 | 最终 Loss | 效果评价 |
|---|---|---|
| lr=0.01, epochs=2000 | 0.000685 | ✓ 优秀 |
| lr=0.001, epochs=2000 | ~0.01 | △ 收敛慢 |
| lr=0.1, epochs=2000 | ~0.001 | ✓ 可接受(可能震荡) |
| lr=1.0, epochs=2000 | NaN | ✗ 发散 |